/*
 * Copyright 2001-2005 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.gitee.booting.plugins.obscure;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;

import javax.net.ssl.*;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * Start allatori in a new process to obscure the target jar in current module.
 */
@Mojo(name = "obscure", defaultPhase = LifecyclePhase.DEPLOY)
public class ObscureMojo extends AbstractMojo {
    private static final Map<String, Map<String, String>> PACKAGING_CONVERTER = new HashMap<>();

    /**
     * Location of allatori configuration template, see {@link URL#openConnection()}.
     */
    @Parameter(property = "allatori.remote")
    private String remote;
    /**
     * Using a local configuration file instead.
     */
    @Parameter(property = "allatori.config", defaultValue = "allatori-config.xml")
    private String config;
    /**
     * Indicator to skip all executions.
     */
    @Parameter(property = "allatori.skip", defaultValue = "false")
    private boolean skip;
    /**
     * Indicator to skip verify SSL certificate if {@link #remote} represents a HTTPS resource.
     */
    @Parameter(property = "allatori.skipVerify", defaultValue = "true")
    private boolean skipVerify;
    /**
     * Executions will be executed only if these packaging type.
     */
    @Parameter(property = "allatori.packaging.includes", defaultValue = "jar,war,ear,maven-plugin")
    private List<String> packagingIncludes;
    /**
     * Watermark `key` property for allatori, see http://www.allatori.com/doc.html#watermark.
     */
    @Parameter(property = "allatori.watermark.key", defaultValue = "group")
    private String watermarkKey;
    /**
     * Watermark `value` property for allatori, see http://www.allatori.com/doc.html#watermark.
     */
    @Parameter(property = "allatori.watermark.value")
    private String watermarkValue;
    /**
     * These package patterns will be ignored in obscure.
     */
    @Parameter(property = "allatori.ignore")
    private List<String> ignore;
    /**
     * String contains comma separated mappings about packaging to extension to detect target package.
     * For example: maven-plugin=jar,anyKey=anyValue
     */
    @Parameter(property = "allatori.packaging.map", defaultValue = "maven-plugin=jar")
    private String packagingMap;
    /**
     * Indicator to using cache or not.
     */
    @Parameter(property = "allatori.cache", defaultValue = "true")
    private boolean cache;
    /**
     * Regular expression to indicate whether current module should be obscured or not.
     */
    @Parameter(property = "allatori.modules", defaultValue = "^.*$")
    private String modules;

    @Parameter(property = "project.build.directory", readonly = true)
    private File directory;
    @Parameter(property = "project.basedir", readonly = true)
    private File basedir;
    @Parameter(property = "project.build.outputDirectory", readonly = true)
    private File outputDirectory;
    @Parameter(property = "project.build.finalName", readonly = true)
    private String finalName;
    @Parameter(property = "project.packaging", readonly = true)
    private String packaging;
    @Parameter(property = "project.groupId", readonly = true)
    private String groupId;
    @Parameter(property = "project.artifactId", readonly = true)
    private String artifactId;

    private static final String CONFIG = "allatori-config.xml";

    public void execute() throws MojoExecutionException {
        if (skip) {
            getLog().info("Skip execution as required(allatori.skip)");
            return;
        }
        if (!packagingIncludes.contains(packaging)) {
            getLog().info("Skip for packaging type [" + packaging + "], ["
                + String.join(",", packagingIncludes) + "] expected.");
            return;
        }

        final Pattern modules = Pattern.compile(this.modules);
        if (!modules.matcher(this.artifactId).matches()) {
            getLog().info("Skip for artifact name not match : " + this.modules);
            return;
        }

        File f = outputDirectory;
        if (!f.exists()) {
            if (!f.mkdirs())
                throw new RuntimeException("Cannot create folder " + f);
        }

        String extension = detect(packagingMap, packaging);
        Path target = Paths.get(directory.getAbsolutePath(), finalName + "." + extension);
        Path backup = Paths.get(directory.getAbsolutePath(), finalName + "-origin." + extension);
        Path configCurrent = Files.exists(Paths.get(this.config)) ? Paths.get(this.config) :
            Paths.get(basedir.getAbsolutePath(), this.config);

        if (!Files.exists(configCurrent)) {
            configCurrent = Paths.get(directory.getAbsolutePath(), CONFIG);
            File configLocal = new File(System.getProperty("user.home"), CONFIG);
            if (!configLocal.exists() || !cache) {
                InputStream xml;
                String log;
                if (null != remote && remote.trim().length() > 0) {
                    if (skipVerify)
                        disableSSL();
                    final URL url;
                    final URLConnection connection;
                    try {
                        url = new URL(remote.trim());
                        connection = url.openConnection();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                    connection.setDoInput(true);

                    try {
                        xml = connection.getInputStream();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                    log = "Get allatori configuration from [" + remote + "] to [" + configLocal.getAbsolutePath() + "]";
                } else {
                    xml = getClass().getClassLoader().getResourceAsStream("config.xml");
                    log = "Extract allatori configuration from [classpath:config.xml] to [" + configLocal.getAbsolutePath() + "]";
                }

                try {
                    copy(xml, new FileOutputStream(configLocal));
                    getLog().info(log);
                } catch (IOException e) {
                    throw new MojoExecutionException(e.getLocalizedMessage());
                }
            } else {
                getLog().info("Found allatori template : " + configLocal.getAbsolutePath());
            }

            if (!configCurrent.toFile().exists() || !cache) {
                SAXReader reader = new SAXReader();
                Document document;
                try {
                    document = reader.read(configLocal);
                } catch (DocumentException e) {
                    throw new MojoExecutionException(e.getLocalizedMessage());
                }
                Element root = document.getRootElement();

                // update in/out jar name
                Element jar = root.element("input").element("jar");
                jar.addAttribute("in", backup.toFile().getAbsolutePath());
                jar.addAttribute("out", target.toFile().getAbsolutePath());

                // generating watermark information, see http://www.allatori.com/features/watermarking.html
                Element watermark = root.element("watermark");
                watermark.addAttribute("key", watermarkKey);
                watermark.addAttribute("value",
                    null != watermarkValue && watermarkValue.trim().length() > 0 ? watermarkValue :
                        String.join(".", reverse(groupId.split("\\."))));

                ((List<Element>) root.elements("property")).forEach(e -> {
                    // add random-seed string
                    if ("random-seed".equals(e.attribute("name").getValue())) {
                        // groupId:artifactId-version[-classifier].packaging
                        e.addAttribute("value", groupId + ":" + finalName + "." + extension);
                    }
                });

                // build ignoring classes
                Element ignoreClasses = root.element("ignore-classes");
                for (String pattern : ignore) {
                    ignoreClasses.addElement("class").addAttribute("template", pattern);
                }

                try {
                    XMLWriter writer = new XMLWriter(new OutputStreamWriter(
                        new FileOutputStream(configCurrent.toFile()), StandardCharsets.UTF_8));
                    writer.write(document);
                    writer.flush();
                    writer.close();
                    getLog().info("Built configuration file to : " + configCurrent.toFile().getAbsolutePath());
                } catch (IOException e) {
                    throw new MojoExecutionException(e.getLocalizedMessage());
                }
            } else {
                getLog().info("Using existing configuration file : " + configCurrent.toFile().getAbsolutePath());
            }
            try {
                Files.move(target, backup);
                getLog().info("Backup origin target [" + target.toFile().getAbsolutePath()
                    + "] to [" + backup.toFile().getAbsolutePath() + "]");
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        } else {
            getLog().info("Using local configuration file : " + configCurrent.toFile().getAbsolutePath());
        }

        // extract executable
        Path lib = Paths.get(System.getProperty("user.home"), "allatori");
        if (!lib.toFile().exists()) {
            lib.toFile().mkdirs();
        }
        Path allatori = lib.resolve("allatori.jar");
        Path allatoriAnnotation = lib.resolve("allatori-annotations.jar");
        try {
            if (!allatori.toFile().exists() || !cache) {
                FileOutputStream fos = new FileOutputStream(allatori.toFile());
                InputStream is = getClass().getClassLoader().getResourceAsStream("allatori.jar");
                copy(is, fos);
                getLog().info("Extracted allatori.jar to : " + allatori.toFile().getAbsolutePath());
            } else {
                getLog().info("Found allatori.jar at : " + allatori.toFile().getAbsolutePath());
            }
            if (!allatoriAnnotation.toFile().exists() || !cache) {
                FileOutputStream fos = new FileOutputStream(allatoriAnnotation.toFile());
                InputStream is = getClass().getClassLoader().getResourceAsStream("allatori-annotations.jar");
                copy(is, fos);
                getLog().info("Extracted allatori-annotations.jar to : " + allatoriAnnotation.toFile().getAbsolutePath());
            } else {
                getLog().info("Found allatori-annotations.jar at : " + allatoriAnnotation.toFile().getAbsolutePath());
            }
        } catch (IOException e) {
            throw new MojoExecutionException(e.getLocalizedMessage());
        }

        Process ps;
        try {
            String[] cmd = new String[]{
                "java", "-classpath", allatoriAnnotation.toFile().getAbsolutePath(),
                "-jar", allatori.toFile().getAbsolutePath(),
                configCurrent.toFile().getAbsolutePath()
            };
            getLog().info("Starting process with arguments as : " + String.join(" ", cmd));
            ps = Runtime.getRuntime().exec(cmd, null, directory);
            ps.waitFor();
            copy(ps.getInputStream(), System.out);
        } catch (IOException | InterruptedException e) {
            throw new MojoExecutionException(e.getLocalizedMessage());
        }
    }

    private static void disableSSL() {
        try {
            TrustManager[] trustAllCerts = new TrustManager[]{new AllowAllTrustManager()};
            // Install the all-trusting trust manager
            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, trustAllCerts, new java.security.SecureRandom());
            HostnameVerifier allHostsValid = new HostnameVerifier() {
                public boolean verify(String hostname, SSLSession session) {
                    return true;
                }
            };
            HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    static class AllowAllTrustManager implements X509TrustManager {
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }

        public void checkClientTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString) throws CertificateException {
        }

        public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
        }
    }

    public static int copy(InputStream in, OutputStream out) throws IOException {
        int byteCount = 0;
        byte[] buffer = new byte[4096];

        int bytesRead;
        for (boolean var4 = true; (bytesRead = in.read(buffer)) != -1; byteCount += bytesRead) {
            out.write(buffer, 0, bytesRead);
        }

        out.flush();
        return byteCount;
    }

    public static <T> T[] reverse(T[] array) {
        if (null != array) {
            T t;
            for (int i = 0, j = array.length - 1; i < j; ++i, --j) {
                t = array[i];
                array[i] = array[j];
                array[j] = t;
            }
        }
        return array;
    }

    public static String detect(String map, String key) {
        if (null != map) {
            PACKAGING_CONVERTER.computeIfAbsent(map, m -> new HashMap<>());
            for (String one : map.split(",")) {
                String[] kv = one.split("=");
                if (kv.length >= 2 && kv[0].equals(key) && kv[1].length() > 0) {
                    PACKAGING_CONVERTER.get(map).put(kv[0], kv[1]);
                }
            }
            return PACKAGING_CONVERTER.get(map).getOrDefault(key, key);
        }
        return key;
    }
}
